//-------------------------------------------------------------------------------------
// <copyright file="MsiTaskBase.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation. All rights reserved.
//      THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
//      EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
//      WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// </copyright>
// <author>Greg Beech</author>
// <email>gregbee@microsoft.com</email>
// <date>2004-04-15</date>
// <summary>
//      Base class for tasks that invoke MsiExec to perform actions on MSI packages.
// </summary>  
//-------------------------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Msi
{
    #region Using directives

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Text;
    using System.Xml;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;
    using System.Runtime.InteropServices;

    #endregion

    /// <summary>
    /// Base class for tasks that invoke MsiExec to perform actions on MSI packages.
    /// </summary>
    public abstract class MsiTaskBase : TaskBase
    {
        #region Constants

        /// <summary>
        /// The format to use when converting guids to strings for the MsiExec command line.
        /// </summary>
        protected const string GuidFormat = "B";

        /// <summary>
        /// The exit code from MsiExec if it exits successfully.
        /// </summary>
        private const int MsiExecSuccessCode = 0;

        /// <summary>
        /// The property to for installing features using the MsiExec command line.
        /// </summary>
        private const string MsiPropertyInstallFeatures = "ADDLOCAL";

        /// <summary>
        /// The property to for patching features using the MsiExec command line.
        /// </summary>
        private const string MsiPropertyPatchFeatures = "REINSTALL";

        /// <summary>
        /// The namespace of the schema used for the MSI property files.
        /// </summary>
        private const string PropertySchemaNamespace = "urn:sdc-microsoft-com:deployment:properties:v2.0";

        #endregion

        #region Member Variables

        /// <summary>
        /// internal variable
        /// </summary>
        private bool loadUserProfile = false;
        
        /// <summary>
        /// internal variable
        /// </summary>
        private string logFile;
        
        /// <summary>
        /// internal variable - defaults to MsiLoggingOptions.Default
        /// </summary>
        private string[] logOptions = new string[] { MsiLoggingOptions.Default.ToString() };

        /// <summary>
        /// internal variable
        /// </summary>
        private string msiPath;

        /// <summary>
        /// internal variable
        /// </summary>
        private Guid productCode = Guid.Empty;

        /// <summary>
        /// internal variable
        /// </summary>
        private string[] properties;

        /// <summary>
        /// internal variable
        /// </summary>
        private string[] propertyFiles;

        /// <summary>
        /// internal variable
        /// </summary>
        private MsiExecUILevel userInterfaceLevel = MsiExecUILevel.None;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the TaskBase class.
        /// </summary>
        protected MsiTaskBase()
        {
        }

        #endregion

        #region Enumerations

        #region MsiExecUILevel Enumeration

        /// <summary>
        /// The user interface level for MsiExec.
        /// </summary>
        public enum MsiExecUILevel
        {
            /// <summary>
            /// No user interface is displayed.
            /// </summary>
            None,

            /// <summary>
            /// A basic user interface is displayed.
            /// </summary>
            Basic,

            /// <summary>
            /// A reduced interface is displayed with a modal dialog box at the end.
            /// </summary>
            ReducedWithModalDialog,

            /// <summary>
            /// A full user interface is displayed with a modal dialog box at the end.
            /// </summary>
            FullWithModalDialog,

            /// <summary>
            /// No user interface is displayed but a modal dialog box is shown at the end.
            /// </summary>
            NoneWithModalDialog,

            /// <summary>
            /// A basic user interface is displayed with a modal dialog box at the end.
            /// </summary>
            BasicWithModalDialog,

            /// <summary>
            /// A basic user interface is displayed with no modal dialog boxes being shown.
            /// </summary>
            BasicWithNoModalDialogs
        }

        #endregion

        #region MsiLoggingOptions Enumeration

        /// <summary>
        /// Options for the logging output of an MSI. These options may be combined.
        /// </summary>
        [Flags]
        public enum MsiLoggingOptions
        {
            /// <summary>
            /// Uses the default logging options.
            /// </summary>
            Default = MsiLoggingOptions.AllInformation,

            /// <summary>
            /// Log status messages.
            /// </summary>
            StatusMessages = 0x00000001,

            /// <summary>
            /// Log non-fatal warnings.
            /// </summary>
            Warnings = 0x00000002,

            /// <summary>
            /// Log errors
            /// </summary>
            Errors = 0x00000004,

            /// <summary>
            /// Log the start of actions.
            /// </summary>
            ActionStart = 0x00000008,

            /// <summary>
            /// Log action specific requests.
            /// </summary>
            ActionSpecificRequests = 0x00000010,

            /// <summary>
            /// Log user requests.
            /// </summary>
            UserRequests = 0x00000020,

            /// <summary>
            /// Log initial user interface parameters.
            /// </summary>
            InitialParameters = 0x00000040,

            /// <summary>
            /// Log out of memory conditions.
            /// </summary>
            OutOfMemory = 0x00000080,

            /// <summary>
            /// Log terminal properties.
            /// </summary>
            TerminalProperties = 0x00000100,

            /// <summary>
            /// Log with verbose output.
            /// </summary>
            Verbose = 0x00000200,

            /// <summary>
            /// Append the information to the existing log file rather than overwriting it.
            /// </summary>
            AppendLog = 0x00001000,

            /// <summary>
            /// Immediately flush each line to the log file rather than buffering it.
            /// </summary>
            FlushEachLine = 0x00002000,

            /// <summary>
            /// Log all information except verbose output. Note that this can be combined with the <b>Verbose</b> flag to
            /// log all output in verbose format.
            /// </summary>
            AllInformation =
                MsiLoggingOptions.StatusMessages |
                MsiLoggingOptions.Warnings |
                MsiLoggingOptions.Errors |
                MsiLoggingOptions.ActionStart |
                MsiLoggingOptions.ActionSpecificRequests |
                MsiLoggingOptions.UserRequests |
                MsiLoggingOptions.InitialParameters |
                MsiLoggingOptions.OutOfMemory |
                MsiLoggingOptions.TerminalProperties
        }

        #endregion

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets whether the user's profile is loaded during the processing of the MSI.
        /// </summary>
        /// <remarks>
        /// For some custom installation actions such as configuring ports and channels in Microsoft BizTalk Server
        /// the user profile must be loaded for the actions to succeed. Most installations do not need the user's
        /// profile to be loaded.
        /// </remarks>
        /// <value>
        /// <b>true</b> if the user's profile is loaded, or <b>false</b> otherwise.
        /// </value>
        public bool LoadUserProfile
        {
            get
            {
                return this.loadUserProfile;
            }

            set
            {
                this.loadUserProfile = value;
            }
        }

        /// <summary>
        /// Gets or sets the full or relative path to the log file.
        /// </summary>
        /// <value>
        /// The full or relative path to the log file.
        /// </value>
        [Required]
        public string LogFile
        {
            get
            {
                return (this.logFile == null) ? string.Empty : this.logFile;
            }

            set
            {
                this.logFile = value;
            }
        }

        /// <summary>
        /// Gets or sets the options for logging during the installation process.
        /// </summary>
        /// <value>
        /// The options for logging during the installation process.
        /// </value>
        public string[] LogOptions
        {
            get
            {
                return (this.logOptions == null) ? new string[0] : this.logOptions;
            }

            set
            {
                this.logOptions = value;
            }
        }

        /// <summary>
        /// Gets or sets the full or relative path to the MSI.
        /// </summary>
        /// <value>
        /// The full or relative path to the MSI.
        /// </value>
        public string MsiPath
        {
            get
            {
                return (this.msiPath == null) ? string.Empty : this.msiPath;
            }

            set
            {
                this.msiPath = value;
            }
        }

        /// <summary>
        /// Gets or sets the product code for the product that is installed by the MSI.
        /// </summary>
        /// <value>
        /// The product code for the product that is installed by the MSI.
        /// </value>
        public Guid ProductCode
        {
            get
            {
                return this.productCode;
            }

            set
            {
                this.productCode = value;
            }
        }

        /// <summary>
        /// Gets or sets any properties to pass to the MSI.
        /// </summary>
        /// <remarks>
        /// A list of installation properties in the format Name=Value. If any property files have been 
        /// specified, then the properties specified here are seen as the most significant and can overwrite 
        /// any of the property values specified in the files.
        /// </remarks>
        /// <value>
        /// Properties to pass to the MSI.
        /// </value>
        public string[] Properties
        {
            get
            {
                return (this.properties == null) ? new string[0] : this.properties;
            }

            set
            {
                this.properties = value;
            }
        }

        /// <summary>
        /// Gets or sets the property file(s) used to provide properties to the MSI.
        /// </summary>
        /// <remarks>
        /// Use this property to specify one or more property files which can provide properties for the MSI being installed.
        /// Multiple property files are allowed so that you can have a hierarchy - the first file provides a base set of properties
        /// (e.g. environmental properties for a particular rig) and subsequent files can override these to provide a more
        /// specific property or add new properties (e.g. properties for a machine, then properties for an individual MSI).
        /// </remarks>
        /// <value>
        /// A list of property files.
        /// </value>
        public string[] PropertyFiles
        {
            get
            {
                return (this.propertyFiles == null) ? new string[0] : this.propertyFiles;
            }

            set
            {
                this.propertyFiles = value;
            }
        }

        /// <summary>
        /// Gets or sets the user interface level of MsiExec.
        /// </summary>
        /// <value>
        /// The user interface level of MsiExec.
        /// </value>
        public MsiExecUILevel UserInterfaceLevel
        {
            get
            {
                return this.userInterfaceLevel;
            }

            set
            {
                this.userInterfaceLevel = value;
            }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Appends the logging argument string for MsiExec with the specified options.
        /// </summary>
        /// <param name="arguments">
        /// The string of arguments being built.
        /// </param>
        protected void AppendLoggingArgument(StringBuilder arguments)
        {
            // parse the logging options
            MsiLoggingOptions options = MsiLoggingOptions.Default;
            if (this.logOptions != null && this.logOptions.Length > 0)
            {
                options = 0;
                foreach (string optionString in this.logOptions)
                {
                    options |= (MsiLoggingOptions)Enum.Parse(typeof(MsiLoggingOptions), optionString, true);
                }
            }

            // the standard logging switch
            arguments.Append("/l");

            // logging options related to the information types being logged
            if ((options & MsiLoggingOptions.AllInformation) == MsiLoggingOptions.AllInformation)
            {
                // all information types are wanted so we can just put a * for the info part
                arguments.Append("*");
            }
            else
            {
                // not all information is wanted so we need to filter it
                if ((options & MsiLoggingOptions.StatusMessages) > 0)
                {
                    arguments.Append("i");
                }

                if ((options & MsiLoggingOptions.Warnings) > 0)
                {
                    arguments.Append("w");
                }

                if ((options & MsiLoggingOptions.Errors) > 0)
                {
                    arguments.Append("e");
                }

                if ((options & MsiLoggingOptions.ActionStart) > 0)
                {
                    arguments.Append("a");
                }

                if ((options & MsiLoggingOptions.ActionSpecificRequests) > 0)
                {
                    arguments.Append("r");
                }

                if ((options & MsiLoggingOptions.UserRequests) > 0)
                {
                    arguments.Append("u");
                }

                if ((options & MsiLoggingOptions.InitialParameters) > 0)
                {
                    arguments.Append("c");
                }

                if ((options & MsiLoggingOptions.OutOfMemory) > 0)
                {
                    arguments.Append("m");
                }

                if ((options & MsiLoggingOptions.TerminalProperties) > 0)
                {
                    arguments.Append("p");
                }
            }

            // other options not related to the information types logged
            if ((options & MsiLoggingOptions.Verbose) > 0)
            {
                arguments.Append("v");
            }

            if ((options & MsiLoggingOptions.AppendLog) > 0)
            {
                arguments.Append("+");
            }

            if ((options & MsiLoggingOptions.FlushEachLine) > 0)
            {
                arguments.Append("!");
            }

            // the log file name
            arguments.Append(" ");
            arguments.Append(this.EscapeArgument(this.logFile));
        }

        /// <summary>
        /// Appends the MSI property arguments.
        /// </summary>
        /// <param name="arguments">
        /// The string of arguments being built.
        /// </param>
        /// <param name="properties">
        /// The properties for the MSI to append.
        /// </param>
        [CLSCompliant(false)]
        protected void AppendPropertiesArguments(StringBuilder arguments, Dictionary<string, string> properties)
        {
            int processedItems = 0;
            foreach (KeyValuePair<string, string> prop in properties)
            {
                // check the property
                this.AssertMsiExecPropertyNameIsValid(prop.Key);

                // append it
                arguments.Append(prop.Key);
                arguments.Append("=");
                arguments.Append(this.EscapeArgument(prop.Value));
                if (++processedItems < properties.Count)
                {
                    arguments.Append(" ");
                }
            }
        }

        /// <summary>
        /// Appends only the MSI property arguments that are not in the exclusion list.
        /// </summary>
        /// <param name="arguments">
        /// The string of arguments being built.
        /// </param>
        /// <param name="properties">
        /// The properties for the MSI to append.
        /// </param>
        /// <param name="exclusions">
        /// The properties to be excluded from the command-line.
        /// </param>
        [CLSCompliant(false)]
        protected void AppendPropertiesArguments(StringBuilder arguments, Dictionary<string, string> properties, List<string> exclusions)
        {
            foreach (KeyValuePair<string, string> prop in properties)
            {
                bool isExcluded = false;
                if (exclusions != null)
                {
                    isExcluded = exclusions.Exists(delegate(string match)
                    {
                        return (match.ToUpper() == prop.Key.ToUpper()) ? true : false;
                    });
                }

                if (isExcluded)
                {
                    // check the property
                    this.AssertMsiExecPropertyNameIsValid(prop.Key);

                    // append it
                    arguments.Append(" ");
                    arguments.Append(prop.Key);
                    arguments.Append("=");
                    arguments.Append(this.EscapeArgument(prop.Value));
                }
            }
        }

        /// <summary>
        /// Appends the argument for the user interface level of MsiExec.
        /// </summary>
        /// <param name="arguments">
        /// The string of arguments being built.
        /// </param>
        protected void AppendUserInterfaceLevelArgument(StringBuilder arguments)
        {
            // the standard UI level switch
            arguments.Append("/q");

            // add the level
            switch (this.userInterfaceLevel)
            {
                case MsiExecUILevel.None:
                    arguments.Append("n");
                    break;
                case MsiExecUILevel.Basic:
                    arguments.Append("b");
                    break;
                case MsiExecUILevel.ReducedWithModalDialog:
                    arguments.Append("r");
                    break;
                case MsiExecUILevel.FullWithModalDialog:
                    arguments.Append("f");
                    break;
                case MsiExecUILevel.NoneWithModalDialog:
                    arguments.Append("n+");
                    break;
                case MsiExecUILevel.BasicWithModalDialog:
                    arguments.Append("b+");
                    break;
                case MsiExecUILevel.BasicWithNoModalDialogs:
                    arguments.Append("b-");
                    break;
            }
        }

        /// <summary>
        /// Escapes an argument so that it is interpreted correctly. This does things like enclosing in quotes if there
        /// are spaces and escaping quotes.
        /// </summary>
        /// <param name="argument">
        /// The argument to escape.
        /// </param>
        /// <returns>
        /// The escaped argument.
        /// </returns>
        protected string EscapeArgument(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                return "\"\"";        // a pair of quotes with nothing in between
            }
            else
            {
                // replace any quotes with double quotes
                if (argument.Contains("\""))
                {
                    argument = argument.Replace("\"", "\"\"");
                }

                // wrap with quotes if it contains any spaces
                if (argument.Contains(" "))
                {
                    argument = string.Concat("\"", argument, "\"");
                }

                return argument;
            }
        }

        /// <summary>
        /// Loads all of the properties specified in the files and string literals.
        /// </summary>
        /// <returns>
        /// A dictionary of all the properties.
        /// </returns>
        [CLSCompliant(false)]
        protected Dictionary<string, string> LoadProperties()
        {
            Dictionary<string, string> installationProperties = new Dictionary<string, string>();
            if (this.propertyFiles != null && this.propertyFiles.Length > 0)
            {
                foreach (string filename in this.propertyFiles)
                {
                    this.LoadPropertyFile(filename, installationProperties);
                }
            }

            if (this.properties != null && this.properties.Length > 0)
            {
                this.LoadPropertyStrings(installationProperties);
            }

            return installationProperties;
        }

        /// <summary>
        /// Loads all of the properties specified in the files and string literals.
        /// </summary>
        /// <returns>
        /// A dictionary of all the properties.
        /// </returns>
        [CLSCompliant(false)]
        protected List<string> LoadExclusions()
        {
            List<string> exclusionProperties = new List<string>();
            if (this.propertyFiles != null && this.propertyFiles.Length > 0)
            {
                foreach (string filename in this.propertyFiles)
                {
                    this.LoadPropertyFileExclusions(filename, exclusionProperties);
                }
            }

            return exclusionProperties;
        }

        /// <summary>
        /// Reads properties from a property XML file and loads them into a dictionary. If the dictionary already
        /// has a property with the same name then the property will be overridden. Property names are not case
        /// sensitive.
        /// </summary>
        /// <param name="filename">
        /// The path to the property file.
        /// </param>
        /// <param name="propertyDictionary">
        /// The dictionary to load the properties into.
        /// </param>
        [CLSCompliant(false)]
        protected void LoadPropertyFile(string filename, Dictionary<string, string> propertyDictionary)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentNullException("filename");
            }

            if (propertyDictionary == null)
            {
                throw new ArgumentNullException("propertyDictionary");
            }

            using (FileStream stream = System.IO.File.OpenRead(filename))
            using (XmlTextReader reader = new XmlTextReader(stream))
            {
                reader.WhitespaceHandling = WhitespaceHandling.None;
                reader.MoveToContent();
                reader.ReadStartElement("Properties", MsiTaskBase.PropertySchemaNamespace);
                do
                {
                    string propertyName = reader.GetAttribute("Name");
                    if (propertyDictionary.ContainsKey(propertyName))
                    {
                        propertyDictionary[propertyName] = reader.GetAttribute("Value");
                    }
                    else
                    {
                        propertyDictionary.Add(propertyName, reader.GetAttribute("Value"));
                    }
                }
                while (reader.ReadToNextSibling("Property", MsiTaskBase.PropertySchemaNamespace));
            }
        }

        /// <summary>
        /// Reads properties from a property XML file and loads them into String collection if they have an associated 'Embed' attribute set to 'False'.
        /// </summary>
        /// <param name="filename">
        /// The path to the property file.
        /// </param>
        /// <param name="propertyExclusionDictionary">
        /// The string collection to load the properties into.
        /// </param>
        [CLSCompliant(false)]
        protected void LoadPropertyFileExclusions(string filename, List<string> propertyExclusionDictionary)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentNullException("filename");
            }

            if (propertyExclusionDictionary == null)
            {
                throw new ArgumentNullException("propertyDictionary");
            }

            using (FileStream stream = System.IO.File.OpenRead(filename))
            using (XmlTextReader reader = new XmlTextReader(stream))
            {
                reader.WhitespaceHandling = WhitespaceHandling.None;
                reader.MoveToContent();
                reader.ReadStartElement("Properties", MsiTaskBase.PropertySchemaNamespace);
                do
                {
                    string propertyName = reader.GetAttribute("Name");
                    string excluded = reader.GetAttribute("Embed");

                    if (excluded != null && excluded != String.Empty && excluded.ToUpper() == "FALSE")
                    {
                        if (propertyExclusionDictionary.IndexOf(propertyName) == -1)
                        {
                            propertyExclusionDictionary.Add(propertyName);
                        }
                    }
                }
                while (reader.ReadToNextSibling("Property", MsiTaskBase.PropertySchemaNamespace));
            }
        }

        /// <summary>
        /// Reads properties from the strings and loads them into a dictionary. If the dictionary already
        /// has a property with the same name then the property will be overridden. Property names are case
        /// sensitive.
        /// </summary>
        /// <param name="propertyDictionary">
        /// The dictionary to load the properties into.
        /// </param>
        [CLSCompliant(false)]
        protected void LoadPropertyStrings(Dictionary<string, string> propertyDictionary)
        {
            if (propertyDictionary == null)
            {
                throw new ArgumentNullException("propertyDictionary");
            }

            foreach (string prop in this.properties)
            {
                int equalsIndex = prop.IndexOf('=');
                string propertyName = prop.Substring(0, equalsIndex);
                string propertyValue = (prop.Length > equalsIndex + 1) ? prop.Substring(equalsIndex + 1) : string.Empty;
                if (propertyDictionary.ContainsKey(propertyName))
                {
                    propertyDictionary[propertyName] = propertyValue;
                }
                else
                {
                    propertyDictionary.Add(propertyName, propertyValue);
                }
            }
        }

        /// <summary>
        /// Runs the MsiExec process with the specified arguments, waits for it to finish, and checks that it
        /// was successful.
        /// </summary>
        /// <param name="arguments">
        /// The argument string to pass to MsiExec.
        /// </param>
        /// <exception cref="System.ComponentModel.Win32Exception">
        /// Thrown when the return code of MsiExec indicates that the process has failed.
        /// </exception>
        protected void RunMsiExecProcess(string arguments)
        {
            // check inputs
            if (string.IsNullOrEmpty(arguments))
            {
                throw new ArgumentNullException("arguments");
            }

            // check the arguments length as past experience shows MsiExec hangs if it is too long
            const int MaxSafeArgumentsLength = 4096;
            if (arguments.Length > MaxSafeArgumentsLength)
            {
                this.Log.LogWarning(string.Format(
                    CultureInfo.InvariantCulture,
                    "The length of the MsiExec command line arguments is {0} characters. Arguments greater than {1} characters may cause MsiExec to hang without any errors displayed.",
                    arguments.Length,
                    MaxSafeArgumentsLength));
            }

            // create the startup info
            string executablePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "MsiExec.exe");
            this.Log.LogMessage(MessageImportance.High, string.Format(CultureInfo.InvariantCulture, "Running \"{0}\" with arguments \"{1}\".", executablePath, arguments));
            ProcessStartInfo startInfo = new ProcessStartInfo(executablePath, arguments);
            startInfo.CreateNoWindow = true;
            startInfo.ErrorDialog = false;
            startInfo.LoadUserProfile = this.LoadUserProfile;
            startInfo.UseShellExecute = false;
            startInfo.WindowStyle = ProcessWindowStyle.Hidden;
            if (!string.IsNullOrEmpty(this.MsiPath))
            {
                startInfo.WorkingDirectory = Path.GetDirectoryName(this.MsiPath);
            }

            Process msiExecProcess = null;

            // on a few machines we get intermittant 1619 returns
            // hence we retry twice if we get this error code
            for (int i = 0; i < 3; i++)
            {
                // create the process and wait for it to finish
                msiExecProcess = Process.Start(startInfo);
                msiExecProcess.WaitForExit();

                if (msiExecProcess.ExitCode != 1619)
                {
                    break;
                }
            }

            // check the process return code and throw an exception if it is not the success code
            if (msiExecProcess.ExitCode != MsiTaskBase.MsiExecSuccessCode)
            {
                throw new Win32Exception(msiExecProcess.ExitCode);
            }
        }

        /// <summary>
        /// Build the msi execute arguments collection and embed properties into the named MSI
        /// </summary>
        /// <param name="embedProperties">true- embed properties into the MSI </param>
        /// <param name="productCode">the product code to package</param>
        /// <param name="features">a list of features</param>
        /// <returns>a argument string</returns>
        protected string BuildInstallArgumentsAndEmbedProperties(bool embedProperties, Guid productCode, string[] features)
        {
            Dictionary<string, string> installationProperties = this.LoadProperties();
            List<string> exclusionProperties = this.LoadExclusions();
            string msiExecArguments = null;
            if (!string.IsNullOrEmpty(this.MsiPath))
            {
                if (embedProperties == false)
                {
                    msiExecArguments = this.GetInstallArguments(this.MsiPath, installationProperties, features);
                }
                else
                {
                    msiExecArguments = this.GetInstallArguments(this.MsiPath, installationProperties, exclusionProperties, features);

                    ComWrapper installer = new ComWrapper("WindowsInstaller.Installer");
                    ComWrapper msiDatabase = new ComWrapper(installer.CallMethod("OpenDatabase", new object[] { this.MsiPath, 1 }));

                    if (installationProperties != null && installationProperties.Count > 0)
                    {
                        foreach (KeyValuePair<string, string> prop in installationProperties)
                        {
                            if (exclusionProperties.IndexOf(prop.Key) == -1)
                            {
                                this.AddPropertyRecord(msiDatabase, prop.Key, this.EscapeArgument(prop.Value));
                            }
                        }
                    }

                    msiDatabase.CallMethod("Commit");

                    // We need to free the connection to the database so that the msi can be loaded by the msiexec tool.
                    msiDatabase.ComObject = null;
                    msiDatabase = null;
                    installer.ComObject = null;
                    installer = null;

                    // Make sure the installer' database object has been freed.
                    GC.Collect();
                }
            }
            else if (productCode != Guid.Empty)
            {
                msiExecArguments = this.GetInstallArguments(productCode, installationProperties, features);
            }
            else
            {
                throw new InvalidOperationException("Either the MSI path or the product code must be specified before the task can execute.");
            }

            return msiExecArguments;
        }

        /// <summary>
        /// Build the msp execute arguments collection and extract properties 
        /// </summary>
        /// <param name="features">
        /// The features to be installed.
        /// </param>
        /// <returns>a argument string</returns>
        protected string BuildInstallPatchArgumentsAndExtractProperties(string[] features)
        {
          Dictionary<string, string> installationProperties = this.LoadProperties();
          List<string> exclusionProperties = this.LoadExclusions();
          string msiExecArguments = null;
          if (!string.IsNullOrEmpty(this.MsiPath))
          {
              msiExecArguments = this.GetInstallPatchArguments(this.MsiPath, installationProperties, exclusionProperties, features);
          }
          else
          {
            throw new InvalidOperationException("The MSP path must be specified before the task can execute.");
          }

          return msiExecArguments;
        }

        /// <summary>
        /// build uninstall command line and embed properties into MSI file
        /// </summary>
        /// <param name="embedProperties">true - embed propeties into MSI file</param>
        /// <returns>a command line string for use with the MSI</returns>
        protected string BuildUninstallArgumentsAndEmbedProperties(bool embedProperties)
        {
            Dictionary<string, string> installationProperties = this.LoadProperties();
            List<string> exclusionProperties = this.LoadExclusions();
            string msiExecArguments = null;
            if (!string.IsNullOrEmpty(this.MsiPath))
            {
                if (embedProperties == false)
                {
                    msiExecArguments = this.GetUninstallArguments(this.MsiPath, installationProperties);
                }
                else
                {
                    msiExecArguments = this.GetUninstallArguments(this.MsiPath, null);

                    ComWrapper installer = new ComWrapper("WindowsInstaller.Installer");
                    ComWrapper msiDatabase = new ComWrapper(installer.CallMethod("OpenDatabase", new object[] { this.MsiPath, 1 }));

                    if (installationProperties != null && installationProperties.Count > 0)
                    {
                        foreach (KeyValuePair<string, string> prop in installationProperties)
                        {
                            if (exclusionProperties.IndexOf(prop.Key) == -1)
                            {
                                this.AddPropertyRecord(msiDatabase, prop.Key, this.EscapeArgument(prop.Value));
                            }
                        }
                    }

                    msiDatabase.CallMethod("Commit");

                    // We need to free the connection to the database so that the msi can be loaded by the msiexec tool.
                    msiDatabase.ComObject = null;
                    msiDatabase = null;
                    installer.ComObject = null;
                    installer = null;

                    // Make sure the installer' database object has been freed.
                    GC.Collect();
                }
            }
            else if (this.ProductCode != Guid.Empty)
            {
                msiExecArguments = this.GetUninstallArguments(this.ProductCode, installationProperties);
            }
            else
            {
                throw new InvalidOperationException("Either the MSI path or the product code must be specified before the task can execute.");
            }

            return msiExecArguments;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Asserts whether an MsiExec property name is valid and throws an error if not. If there are
        /// any warnings about the name but it is still valid these will be logged.
        /// </summary>
        /// <param name="propertyName">
        /// The name of the property.
        /// </param>
        private void AssertMsiExecPropertyNameIsValid(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ApplicationException("At least one MsiExec property has a null or zero-length name.");
            }
            else if (propertyName.Contains(" "))
            {
                throw new ApplicationException(string.Format(
                    CultureInfo.InvariantCulture,
                    "The MsiExec property name \"{0}\" contains an invalid space character.",
                    propertyName));
            }
            else if (propertyName.ToUpperInvariant() != propertyName)
            {
                // this may cause problems but it won't cause MsiExec to fail so we'll just log a warning
                this.Log.LogWarning(string.Format(
                    CultureInfo.InvariantCulture,
                    "The MsiExec property name \"{0}\" is not all upper case which may cause the value to be set incorrectly. Please see KB article Q230781 for details.",
                    propertyName));
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Add entries to the property database
        /// </summary>
        /// <param name="msiDatabase">a reference to the MSI object</param>
        /// <param name="propertyName">the name of the that is being added</param>
        /// <param name="propertyValue">the value of the property that is being added</param>
        private void AddPropertyRecord(ComWrapper msiDatabase /* WindowsInstaller.Database msiDatabase */, string propertyName, string propertyValue)
        {
            ComWrapper propertyView; // WindowsInstaller.View propertyView;
            string msiSQLStatement;

            // MSI throws if we try to set a property to an empty string value, so substitute a space
            if (0 == propertyValue.Length) 
            { 
                propertyValue = " "; 
            }

            msiSQLStatement = "SELECT * FROM Property WHERE Property = '" + propertyName + "'";
            propertyView = new ComWrapper(msiDatabase.CallMethod("OpenView", new object[] { msiSQLStatement }));
            propertyView.CallMethod("Execute", new object[] { null });
            object pr = propertyView.CallMethod("Fetch");

            if (pr == null)
            {
                // Property DOES NOT exist in database - do INSERT
                msiSQLStatement = "INSERT INTO Property ( Property, Value ) VALUES ( '" + propertyName + "', '" + propertyValue + "' )";
            }
            else
            {
                // Property DOES exist
                msiSQLStatement = "UPDATE Property SET Value = '" + propertyValue + "' WHERE Property = '" + propertyName + "'";
            }

            propertyView = new ComWrapper(msiDatabase.CallMethod("OpenView", new object[] { msiSQLStatement }));
            propertyView.CallMethod("Execute", new object[] { null });
            propertyView.CallMethod("Close");
        }

        /// <summary>
        /// Appends the arguments to select features to be installed from an MSI.
        /// </summary>
        /// <param name="arguments">
        /// The argument string being built.
        /// </param>
        /// <param name="propertyName">
        /// The name of the property that controls the feature list. For example,
        /// <seealso cref="MsiTaskBase.MsiPropertyInstallFeatures">ADDLOCAL</seealso> or
        /// <seealso cref="MsiTaskBase.MsiPropertyPatchFeatures">REINSTALL</seealso>.
        /// </param>
        /// <param name="features">a list of features</param>
        private void AppendFeaturesArgument(StringBuilder arguments, string propertyName, string[] features)
        {
            arguments.AppendFormat("{0}=\"", propertyName);
            for (int i = 0; i < features.Length; i++)
            {
                arguments.Append(features[i]);
                if (i < (features.Length - 1))
                {
                    arguments.Append(",");
                }
            }

            arguments.Append("\"");
        }

        /// <summary>
        /// Builds the arguments to install a product.
        /// </summary>
        /// <param name="productCode">
        /// The identifier of the product.
        /// </param>
        /// <param name="properties">
        /// The dictionary of properties to pass to the MSI, this may be <b>null</b> if the MSI does not need any properties passed.
        /// </param>
        /// <param name="features">a list of features</param>
        /// <returns>
        /// The argument string to install the product.
        /// </returns>
        private string GetInstallArguments(Guid productCode, Dictionary<string, string> properties, string[] features)
        {
            string productCodeString = productCode.ToString(MsiTaskBase.GuidFormat, CultureInfo.InvariantCulture);
            return this.GetInstallArguments(productCodeString, properties, features);
        }

        /// <summary>
        /// Builds the arguments to install a product.
        /// </summary>
        /// <param name="packageName">
        /// The name of the package, e.g. <i>C:\Install\Source\MyPackage.msi</i>
        /// </param>
        /// <param name="properties">
        /// The dictionary of properties to pass to the MSI, this may be <b>null</b> if the MSI does not need any properties passed.
        /// </param>
        /// <param name="features">a list of features</param>
        /// <returns>
        /// The argument string to install the product.
        /// </returns>
        private string GetInstallArguments(string packageName, Dictionary<string, string> properties, string[] features)
        {
            StringBuilder arguments = new StringBuilder("/i ");
            arguments.Append(this.EscapeArgument(packageName));
            arguments.Append(" ");
            this.AppendLoggingArgument(arguments);
            arguments.Append(" ");
            this.AppendUserInterfaceLevelArgument(arguments);
            if (features != null && features.Length > 0)
            {
                arguments.Append(" ");
                this.AppendFeaturesArgument(arguments, MsiTaskBase.MsiPropertyInstallFeatures, features);
            }

            if (properties != null && properties.Count > 0)
            {
                arguments.Append(" ");
                this.AppendPropertiesArguments(arguments, properties);
            }

            return arguments.ToString();
        }

        /// <summary>
        /// Builds the arguments to install a patch.
        /// </summary>
        /// <param name="packageName">
        /// The name of the package, e.g. <i>C:\Install\Source\MyPackage.msp</i>
        /// </param>
        /// <param name="properties">
        /// The dictionary of properties to pass to the MSI, this may be <b>null</b> if the MSI does not need any properties passed.
        /// </param>
        /// <param name="exclusions">
        /// The list of properties to not pass to the MSP, this may be <b>null</b> if all properties should be passed.  This is used
        /// only when the property to be excluded has been embedded into the MSP.
        /// </param>
        /// <param name="features">
        /// The features to be installed.
        /// </param>
        /// <returns>
        /// The argument string to install the patch.
        /// </returns>
        private string GetInstallPatchArguments(string packageName, Dictionary<string, string> properties, List<string> exclusions, string[] features)
        {
          StringBuilder arguments = new StringBuilder("/update ");
          arguments.Append(this.EscapeArgument(packageName));
          arguments.Append(" ");
          this.AppendLoggingArgument(arguments);
          arguments.Append(" ");
          this.AppendUserInterfaceLevelArgument(arguments);
          if (features != null && features.Length > 0)
          {
              arguments.Append(" ");
              this.AppendFeaturesArgument(arguments, MsiTaskBase.MsiPropertyPatchFeatures, features);
          }

          if (properties != null && properties.Count > 0)
          {
            arguments.Append(" ");
            this.AppendPropertiesArguments(arguments, properties, exclusions);
          }

          return arguments.ToString();
        }

        /// <summary>
        /// Builds the arguments to install a product.
        /// </summary>
        /// <param name="packageName">
        /// The name of the package, e.g. <i>C:\Install\Source\MyPackage.msi</i>
        /// </param>
        /// <param name="properties">
        /// The dictionary of properties to pass to the MSI, this may be <b>null</b> if the MSI does not need any properties passed.
        /// </param>
        /// <param name="exclusions">
        /// The list of properties to not pass to the MSI, this may be <b>null</b> if all properties should be passed.  This is used
        /// only when the property to be excluded has been embedded into the MSI.
        /// </param>
        /// <param name="features">a list of features</param>
        /// <returns>
        /// The argument string to install the product.
        /// </returns>
        private string GetInstallArguments(string packageName, Dictionary<string, string> properties, List<string> exclusions, string[] features)
        {
            StringBuilder arguments = new StringBuilder("/i ");
            arguments.Append(this.EscapeArgument(packageName));
            arguments.Append(" ");
            this.AppendLoggingArgument(arguments);
            arguments.Append(" ");
            this.AppendUserInterfaceLevelArgument(arguments);
            if (features != null && features.Length > 0)
            {
                arguments.Append(" ");
                this.AppendFeaturesArgument(arguments, MsiTaskBase.MsiPropertyInstallFeatures, features);
            }

            if (properties != null && properties.Count > 0)
            {
                this.AppendPropertiesArguments(arguments, properties, exclusions);
            }

            return arguments.ToString();
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Builds the arguments to uninstall a product.
        /// </summary>
        /// <param name="productCode">
        /// The identifier of the product.
        /// </param>
        /// <param name="properties">
        /// The dictionary of properties to pass to the MSI, this may be <b>null</b> if the MSI does not need any properties passed.
        /// </param>
        /// <returns>
        /// The argument string to uninstall the product.
        /// </returns>
        private string GetUninstallArguments(Guid productCode, Dictionary<string, string> properties)
        {
            string productCodeString = productCode.ToString(MsiTaskBase.GuidFormat, CultureInfo.InvariantCulture);
            return this.GetUninstallArguments(productCodeString, properties);
        }

        /// <summary>
        /// Builds the arguments to uninstall a product.
        /// </summary>
        /// <param name="packageName">
        /// The name of the package, e.g. <i>C:\Install\Source\MyPackage.msi</i>
        /// </param>
        /// <param name="properties">
        /// The dictionary of properties to pass to the MSI, this may be <b>null</b> if the MSI does not need any properties passed.
        /// </param>
        /// <returns>
        /// The argument string to uninstall the product.
        /// </returns>
        private string GetUninstallArguments(string packageName, Dictionary<string, string> properties)
        {
            StringBuilder arguments = new StringBuilder("/x ");
            arguments.Append(this.EscapeArgument(packageName));
            arguments.Append(" ");
            this.AppendLoggingArgument(arguments);
            arguments.Append(" ");
            this.AppendUserInterfaceLevelArgument(arguments);
            if (properties != null && properties.Count > 0)
            {
                arguments.Append(" ");
                this.AppendPropertiesArguments(arguments, properties);
            }

            return arguments.ToString();
        }
        #endregion
    }
}
